<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>股权穿透图</title>
  </head>
  <script src="./d3.v7.min.js"></script>
  <style>
    html,
    body {
      margin: 0;
    }

    html,body {
      width: 100%;
      height: 100%;
      background-color: #e1e1e1;
    }

    #app {
      width: 1600px;
      height: 800px;
      overflow: hidden;
      box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
      box-sizing: border-box;
      background-color: #ffffff;
      border-radius: 8px;
      position: relative;
      margin: 0 auto;
    }
  </style>
  <body>
    <div id="app"></div>
  </body>
  <script>
    let data = {
      id: "abc1005",
      // 根节点名称
      name: "山东吠舍科技有限责任公司",
      // 子节点列表
      children: [
        {
          id:"abc1006",
          name: "山东第一首陀罗科技服务有限公司",
          percent: '100%',
        },
        {
          id:"abc1007",
          name: "山东第二首陀罗程技术有限公司",
          percent: '100%',
        },
        {
          id:"abc1008",
          name: "山东第三首陀罗光伏材料有限公司",
          percent: '100%',
        },
        {
          id:"abc1009",
          name: "山东第四首陀罗科技发展有限公司",
          percent: '100%',
          children: [
            {
              id:"abc1010",
              name: "山东第一达利特瑞利分析仪器有限公司",
              percent: '100%',
              children:[
                {
                  id:"abc1011",
                  name: "山东瑞利的子公司一",
                  percent: '80%',
                },
                {
                  id:"abc1012",
                  name: "山东瑞利的子公司二",
                  percent: '90%',
                },
                {
                  id:"abc1013",
                  name: "山东瑞利的子公司三",
                  percent: '100%',
                },
              ]
            }
          ]
        },
        {
          id:"abc1014",
          name: "山东第五首陀罗电工科技有限公司",
          percent: '100%',
          children: [
            {
              id:"abc1015",
              name: "山东第二达利特低自动化设备有限公司",
              percent: '100%',
              children:[
                {
                  id:"abc1016",
                  name: "山东敬业的子公司一",
                  percent: '100%',
                },
                {
                  id:"abc1017",
                  name: "山东敬业的子公司二",
                  percent: '90%',
                }
              ]
            }
          ]
        },
        {
          id: "abc1020",
          name: "山东第六首陀罗分析仪器(集团)有限责任公司",
          percent: '100%',
          children: [
            {
              id:"abc1021",
              name: "山东第三达利特分气体工业有限公司",
            }
          ]
        },
      ],
      // 父节点列表
      parents: [
        {
          id: "abc2001",
          name: "山东刹帝利集团有限责任公司",
          percent: '60%',
          parents: [
            {
              id: "abc2000",
              name: "山东婆罗门集团有限公司",
              percent: '100%',
            },
          ]
        },
        {
          id: "abc2002",
          name: "吴小远",
          percent: '40%',
        }
      ],
    }
  </script>
  <script>
    // 股权树
    class StockTree {
      constructor(options) {
        // 树的源数据
        this.originTreeData = options.originTreeData;
        // 宿主元素选择器
        this.el = options.el;
        this.nodeClickEvent = options.nodeClickEvent || function (e,d){
          alert(d.name)
        }
        // 一些配置项
        this.config = {
          // 节点的横向距离
          dx: 200,
          // 节点的纵向距离
          dy: 170,
          // svg的viewBox的宽度
          width: 0,
          // svg的viewBox的高度
          height: 0,
          // 节点的矩形框宽度
          rectWidth: 170,
          // 节点的矩形框高度
          rectHeight: 70,
        };
        this.svg = null;
        this.gAll = null;
        this.gLinks = null;
        this.gNodes = null;
        // 给树加坐标点的方法
        this.tree = null;
        // 投资公司树的根节点
        this.rootOfDown = null;
        // 股东树的根节点
        this.rootOfUp = null;

        this.drawChart({
          type: 'fold',
        });
      }

      // 初始化树结构数据
      drawChart(options) {
        // 宿主元素的d3选择器对象
        let host = d3.select(this.el);
        // 宿主元素的DOM，通过node()获取到其DOM元素对象
        let dom = host.node();
        // 宿主元素的DOMRect
        let domRect = dom.getBoundingClientRect();
        // svg的宽度和高度
        this.config.width = domRect.width;
        this.config.height = domRect.height;

        let oldSvg = d3.select('svg')
        // 如果宿主元素中包含svg标签了，那么则删除这个标签，再重新生成一个
        if(!oldSvg.empty()){
          oldSvg.remove();
        }

        const svg = d3
          .create("svg")
          .attr("viewBox", () => {
            let parentsLength = this.originTreeData.parents ? this.originTreeData.parents.length : 0;
            return [
              -this.config.width / 2,
                // 如果有父节点，则根节点居中，否则根节点上浮一段距离
              parentsLength > 0 ? -this.config.height / 2 : -this.config.height / 3,
              this.config.width,
              this.config.height,
            ]
          })
          .style("user-select", "none")
          .style("cursor","move");

        // 包括连接线和节点的总集合
        const gAll = svg.append("g").attr("id", "all");
        svg.call(
          d3
            .zoom()
            .scaleExtent([0.2, 5])
            .on("zoom", (e) => {
              gAll.attr("transform", () => {
                return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`;
              });
            })
        ).on("dblclick.zoom", null);// 取消默认的双击放大事件

        this.gAll = gAll;
        // 连接线集合
        this.gLinks = gAll.append("g").attr("id", "linkGroup");
        // 节点集合
        this.gNodes = gAll.append("g").attr("id", "nodeGroup");
        // 设置好节点之间距离的tree方法
        this.tree = d3.tree().nodeSize([this.config.dx, this.config.dy]);

        this.rootOfDown = d3.hierarchy(this.originTreeData,(d) => d.children);
        this.rootOfUp = d3.hierarchy(this.originTreeData,(d) => d.parents);
        this.tree(this.rootOfDown);

        [this.rootOfDown.descendants(), this.rootOfUp.descendants()].forEach((nodes) => {
          nodes.forEach((node) => {
            node._children = node.children || null;
            if(options.type === 'all'){
              //如果是all的话，则表示全部都展开
              node.children = node._children;
            } else if(options.type === 'fold'){ //如果是fold则表示除了父节点全都折叠
              // 将非根节点的节点都隐藏掉（其实对于这个组件来说加不加都一样）
              if (node.depth) {
                node.children = null;
              }
            }
          });
        });

        //箭头(下半部分)
        svg
          .append("marker")
          .attr("id", "markerOfDown")
          .attr("markerUnits", "userSpaceOnUse")
          .attr("viewBox", "0 -5 10 10") //坐标系的区域
          .attr("refX", 55) //箭头坐标
          .attr("refY", 0)
          .attr("markerWidth", 10) //标识的大小
          .attr("markerHeight", 10)
          .attr("orient", "90") //绘制方向，可设定为：auto（自动确认方向）和 角度值
          .attr("stroke-width", 2) //箭头宽度
          .append("path")
          .attr("d", "M0,-5L10,0L0,5") //箭头的路径
          .attr("fill", "#215af3"); //箭头颜色

        //箭头(上半部分)
        svg
          .append("marker")
          .attr("id", "markerOfUp")
          .attr("markerUnits", "userSpaceOnUse")
          .attr("viewBox", "0 -5 10 10") //坐标系的区域
          .attr("refX", -50) //箭头坐标
          .attr("refY", 0)
          .attr("markerWidth", 10) //标识的大小
          .attr("markerHeight", 10)
          .attr("orient", "90") //绘制方向，可设定为：auto（自动确认方向）和 角度值
          .attr("stroke-width", 2) //箭头宽度
          .append("path")
          .attr("d", "M0,-5L10,0L0,5") //箭头的路径
          .attr("fill", "#215af3"); //箭头颜色

        this.svg = svg;
        this.update();
        // 将svg置入宿主元素中
        host.append(function () {
          return svg.node();
        });
      }

      // 更新数据
      update(source) {
        if (!source) {
          source = {
            x0: 0,
            y0: 0,
          };
          // 设置根节点所在的位置（原点）
          this.rootOfDown.x0 = 0;
          this.rootOfDown.y0 = 0;
          this.rootOfUp.x0 = 0;
          this.rootOfUp.y0 = 0;
        }

        let nodesOfDown = this.rootOfDown.descendants().reverse();
        let linksOfDown = this.rootOfDown.links();
        let nodesOfUp = this.rootOfUp.descendants().reverse();
        let linksOfUp = this.rootOfUp.links();

        this.tree(this.rootOfDown);
        this.tree(this.rootOfUp);

        const myTransition = this.svg.transition().duration(500);

        /***  绘制子公司树  ***/
        const node1 = this.gNodes
          .selectAll("g.nodeOfDownItemGroup")
          .data(nodesOfDown, (d) => {
            return d.data.id;
          });

        const node1Enter = node1
          .enter()
          .append("g")
          .attr("class", "nodeOfDownItemGroup")
          .attr("transform", (d) => {
            return `translate(${source.x0},${source.y0})`;
          })
          .attr("fill-opacity", 0)
          .attr("stroke-opacity", 0)
          .style("cursor", "pointer");

        // 外层的矩形框
        node1Enter
          .append("rect")
          .attr("width", (d) => {
            if (d.depth === 0) {
              return (d.data.name.length + 2) * 16;
            }
            return this.config.rectWidth;
          })
          .attr("height", (d) => {
            if (d.depth === 0) {
              return 30;
            }
            return this.config.rectHeight;
          })
          .attr("x", (d) => {
            if (d.depth === 0) {
              return (-(d.data.name.length + 2) * 16) / 2;
            }
            return -this.config.rectWidth / 2;
          })
          .attr("y", (d) => {
            if (d.depth === 0) {
              return -15;
            }
            return -this.config.rectHeight / 2;
          })
          .attr("rx", 5)
          .attr("stroke-width", 1)
          .attr("stroke", (d) => {
            if (d.depth === 0) {
              return "#5682ec";
            }
            return "#7A9EFF";
          })
          .attr("fill", (d) => {
            if (d.depth === 0) {
              return "#7A9EFF";
            }
            return "#FFFFFF";
          })
          .on("click", (e, d) => {
            this.nodeClickEvent(e,d)
          });
        // 文本主标题
        node1Enter
          .append("text")
          .attr("class", "main-title")
          .attr("x", (d) => {
            return 0;
          })
          .attr("y", (d) => {
            if (d.depth === 0) {
              return 5;
            }
            return -14;
          })
          .attr("text-anchor", (d) => {
            return "middle";
          })
          .text((d) => {
            if (d.depth === 0) {
              return d.data.name;
            } else {
              return d.data.name.length > 11
                ? d.data.name.substring(0, 11)
                : d.data.name;
            }
          })
          .attr("fill", (d) => {
            if (d.depth === 0) {
              return "#FFFFFF";
            }
            return "#000000";
          })
          .style("font-size", (d) => (d.depth === 0 ? 16 : 14))
          .style('font-family','黑体')
          .style("font-weight", "bold");
        // 副标题
        node1Enter
          .append("text")
          .attr("class", "sub-title")
          .attr("x", (d) => {
            return 0;
          })
          .attr("y", (d) => {
            return 5;
          })
          .attr("text-anchor", (d) => {
            return "middle";
          })
          .text((d) => {
            if (d.depth !== 0) {
              let subTitle = d.data.name.substring(11);
              if (subTitle.length > 10) {
                return subTitle.substring(0, 10) + "...";
              }
              return subTitle;
            }
          })
          .style("font-size", (d) => 14)
          .style('font-family','黑体')
          .style("font-weight", "bold");

        // 控股比例
        node1Enter
          .append("text")
          .attr("class", "percent")
          .attr("x", (d) => {
            return 12;
          })
          .attr("y", (d) => {
            return -45;
          })
          .text((d) => {
            if (d.depth !== 0) {
              return d.data.percent;
            }
          })
          .attr("fill", "#000000")
          .style('font-family','黑体')
          .style("font-size", (d) => 14);

        // 增加展开按钮
        const expandBtnG = node1Enter
          .append("g")
          .attr("class", "expandBtn")
          .attr("transform", (d) => {
            return `translate(${0},${this.config.rectHeight / 2})`;
          })
          .style("display", (d) => {
            // 如果是根节点，不显示
            if (d.depth === 0) {
              return "none";
            }
            // 如果没有子节点，则不显示
            if (!d._children) {
              return "none";
            }
          })
          .on("click", (e, d) => {
            if (d.children) {
              d._children = d.children;
              d.children = null;
            } else {
              d.children = d._children;
            }
            this.update(d);
          });

        expandBtnG
          .append("circle")
          .attr("r", 8)
          .attr("fill", "#7A9EFF")
          .attr("cy", 8);

        expandBtnG
          .append("text")
          .attr("text-anchor", "middle")
          .attr("fill", "#ffffff")
          .attr("y", 13)
          .style('font-size', 16)
          .style('font-family','微软雅黑')
          .text((d)=>{
            return d.children ? "-" : "+"
          });

        const link1 = this.gLinks
          .selectAll("path.linkOfDownItem")
          .data(linksOfDown, (d) => d.target.data.id);

        const link1Enter = link1
          .enter()
          .append("path")
          .attr("class", "linkOfDownItem")
          .attr("d", (d) => {
            let o = {
              source: {
                x: source.x0,
                y: source.y0,
              },
              target: {
                x: source.x0,
                y: source.y0,
              },
            };
            return this.drawLink(o);
          })
          .attr("fill", "none")
          .attr("stroke", "#7A9EFF")
          .attr("stroke-width", 1)
          .attr("marker-end", "url(#markerOfDown)");

        // 有元素update更新和元素新增enter的时候
        node1
          .merge(node1Enter)
          .transition(myTransition)
          .attr("transform", (d) => {
            return `translate(${d.x},${d.y})`;
          })
          .attr("fill-opacity", 1)
          .attr("stroke-opacity", 1);

        // 有元素消失时
        node1
          .exit()
          .transition(myTransition)
          .remove()
          .attr("transform", (d) => {
            return `translate(${source.x0},${source.y0})`;
          })
          .attr("fill-opacity", 0)
          .attr("stroke-opacity", 0);

        link1.merge(link1Enter).transition(myTransition).attr("d", this.drawLink);

        link1
          .exit()
          .transition(myTransition)
          .remove()
          .attr("d", (d) => {
            let o = {
              source: {
                x: source.x,
                y: source.y,
              },
              target: {
                x: source.x,
                y: source.y,
              },
            };
            return this.drawLink(o);
          });

        /***  绘制股东树  ***/

        nodesOfUp.forEach(node => {
          node.y = -node.y
        })

        const node2 = this.gNodes
            .selectAll("g.nodeOfUpItemGroup")
            .data(nodesOfUp, (d) => {
              return d.data.id;
            });

        const node2Enter = node2
            .enter()
            .append("g")
            .attr("class", "nodeOfUpItemGroup")
            .attr("transform", (d) => {
              return `translate(${source.x0},${source.y0})`;
            })
            .attr("fill-opacity", 0)
            .attr("stroke-opacity", 0)
            .style("cursor", "pointer");

        // 外层的矩形框
        node2Enter
            .append("rect")
            .attr("width", (d) => {
              if (d.depth === 0) {
                return (d.data.name.length + 2) * 16;
              }
              return this.config.rectWidth;
            })
            .attr("height", (d) => {
              if (d.depth === 0) {
                return 30;
              }
              return this.config.rectHeight;
            })
            .attr("x", (d) => {
              if (d.depth === 0) {
                return (-(d.data.name.length + 2) * 16) / 2;
              }
              return -this.config.rectWidth / 2;
            })
            .attr("y", (d) => {
              if (d.depth === 0) {
                return -15;
              }
              return -this.config.rectHeight / 2;
            })
            .attr("rx", 5)
            .attr("stroke-width", 1)
            .attr("stroke", (d) => {
              if (d.depth === 0) {
                return "#5682ec";
              }
              return "#7A9EFF";
            })
            .attr("fill", (d) => {
              if (d.depth === 0) {
                return "#7A9EFF";
              }
              return "#FFFFFF";
            })
            .on("click", (e, d) => {
              this.nodeClickEvent(e,d)
            });
        // 文本主标题
        node2Enter
            .append("text")
            .attr("class", "main-title")
            .attr("x", (d) => {
              return 0;
            })
            .attr("y", (d) => {
              if (d.depth === 0) {
                return 5;
              }
              return -14;
            })
            .attr("text-anchor", (d) => {
              return "middle";
            })
            .text((d) => {
              if (d.depth === 0) {
                return d.data.name;
              } else {
                return d.data.name.length > 11
                    ? d.data.name.substring(0, 11)
                    : d.data.name;
              }
            })
            .attr("fill", (d) => {
              if (d.depth === 0) {
                return "#FFFFFF";
              }
              return "#000000";
            })
            .style("font-size", (d) => (d.depth === 0 ? 16 : 14))
            .style('font-family','黑体')
            .style("font-weight", "bold");
        // 副标题
        node2Enter
            .append("text")
            .attr("class", "sub-title")
            .attr("x", (d) => {
              return 0;
            })
            .attr("y", (d) => {
              return 5;
            })
            .attr("text-anchor", (d) => {
              return "middle";
            })
            .text((d) => {
              if (d.depth !== 0) {
                let subTitle = d.data.name.substring(11);
                if (subTitle.length > 10) {
                  return subTitle.substring(0, 10) + "...";
                }
                return subTitle;
              }
            })
            .style("font-size", (d) => 14)
            .style('font-family','黑体')
            .style("font-weight", "bold");

        // 控股比例
        node2Enter
            .append("text")
            .attr("class", "percent")
            .attr("x", (d) => {
              return 12;
            })
            .attr("y", (d) => {
              return 55;
            })
            .text((d) => {
              if (d.depth !== 0) {
                return d.data.percent;
              }
            })
            .attr("fill", "#000000")
            .style('font-family','黑体')
            .style("font-size", (d) => 14);

        // 增加展开按钮
        const expandBtnG2 = node2Enter
            .append("g")
            .attr("class", "expandBtn")
            .attr("transform", (d) => {
              return `translate(${0},${-this.config.rectHeight / 2})`;
            })
            .style("display", (d) => {
              // 如果是根节点，不显示
              if (d.depth === 0) {
                return "none";
              }
              // 如果没有子节点，则不显示
              if (!d._children) {
                return "none";
              }
            })
            .on("click", (e, d) => {
              if (d.children) {
                d._children = d.children;
                d.children = null;
              } else {
                d.children = d._children;
              }
              this.update(d);
            });

        expandBtnG2
            .append("circle")
            .attr("r", 8)
            .attr("fill", "#7A9EFF")
            .attr("cy", -8);

        expandBtnG2
            .append("text")
            .attr("text-anchor", "middle")
            .attr("fill", "#ffffff")
            .attr("y", -3)
            .style('font-size', 16)
            .style('font-family','微软雅黑')
            .text((d)=>{
              return d.children ? "-" : "+"
            });

        const link2 = this.gLinks
            .selectAll("path.linkOfUpItem")
            .data(linksOfUp, (d) => d.target.data.id);

        const link2Enter = link2
            .enter()
            .append("path")
            .attr("class", "linkOfUpItem")
            .attr("d", (d) => {
              let o = {
                source: {
                  x: source.x0,
                  y: source.y0,
                },
                target: {
                  x: source.x0,
                  y: source.y0,
                },
              };
              return this.drawLink(o);
            })
            .attr("fill", "none")
            .attr("stroke", "#7A9EFF")
            .attr("stroke-width", 1)
            .attr("marker-end", "url(#markerOfUp)");

        // 有元素update更新和元素新增enter的时候
        node2
            .merge(node2Enter)
            .transition(myTransition)
            .attr("transform", (d) => {
              return `translate(${d.x},${d.y})`;
            })
            .attr("fill-opacity", 1)
            .attr("stroke-opacity", 1);

        // 有元素消失时
        node2
            .exit()
            .transition(myTransition)
            .remove()
            .attr("transform", (d) => {
              return `translate(${source.x0},${source.y0})`;
            })
            .attr("fill-opacity", 0)
            .attr("stroke-opacity", 0);

        link2.merge(link2Enter).transition(myTransition).attr("d", this.drawLink);

        link2
            .exit()
            .transition(myTransition)
            .remove()
            .attr("d", (d) => {
              let o = {
                source: {
                  x: source.x,
                  y: source.y,
                },
                target: {
                  x: source.x,
                  y: source.y,
                },
              };
              return this.drawLink(o);
            });

        // node数据改变的时候更改一下加减号
        const expandButtonsSelection = d3.selectAll('g.expandBtn')

        expandButtonsSelection.select('text').transition().text((d) =>{
          return d.children ? "-" : "+";
        })

        this.rootOfDown.eachBefore((d) => {
          d.x0 = d.x;
          d.y0 = d.y;
        });
        this.rootOfUp.eachBefore((d) => {
          d.x0 = d.x;
          d.y0 = d.y;
        });
      }

      // 直角连接线 by wushengyuan
      drawLink({ source, target }) {
        const halfDistance = (target.y - source.y) / 2;
        const halfY = source.y + halfDistance;
        return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`;
      }

      // 展开所有的节点
      expandAllNodes(){
        this.drawChart({
          type: 'all',
        })
      }

      // 将所有节点都折叠
      foldAllNodes(){
        this.drawChart({
          type: 'fold',
        })
      }
    }

    const tree = new StockTree({
      el: "#app",
      originTreeData: data,
      // 节点点击事件
      nodeClickEvent: function (e,d){
        console.log('当前节点的数据：',d)
      }
    });
  </script>
</html>
